@using System.Diagnostics.CodeAnalysis
@using System.Net
@using System.Globalization
@using System.Security.Claims
@namespace Oqtane.UI
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject SiteState SiteState
@inject NavigationManager NavigationManager
@inject INavigationInterception NavigationInterception
@inject ISyncService SyncService
@inject ISiteService SiteService
@inject IPageService PageService
@inject IUserService UserService
@inject IUrlMappingService UrlMappingService
@inject ILogService LogService
@inject ISettingService SettingService
@inject ICookieConsentService CookieConsentService
@inject IJSRuntime JSRuntime
@implements IHandleAfterRender
@implements IDisposable

@if (!string.IsNullOrEmpty(_error))
{
    <ModuleMessage Message="@_error" Type="@MessageType.Warning" />
}

@DynamicComponent

@code {
    private string _absoluteUri;
    private bool _isInternalNavigation = false;
    private bool _navigationInterceptionEnabled;
    private PageState _pagestate;
    private string _error = "";

    [Parameter]
    public string RenderMode { get; set; }

    [Parameter]
    public string Runtime { get; set; }

    [CascadingParameter]
    PageState PageState { get; set; }

    [Parameter]
    public Action<PageState> OnStateChange { get; set; }

    private RenderFragment DynamicComponent { get; set; }

    protected override void OnInitialized()
    {
        _absoluteUri = NavigationManager.Uri;
        NavigationManager.LocationChanged += LocationChanged;

        DynamicComponent = builder =>
        {
            if (PageState != null && !PageState.Refresh)
            {
                builder.OpenComponent(0, typeof(ThemeBuilder));
                builder.CloseComponent();
            }
        };
    }

    public void Dispose()
    {
        NavigationManager.LocationChanged -= LocationChanged;
    }

    protected override async Task OnParametersSetAsync()
    {
        if (PageState == null || PageState.Refresh)
        {
            await Refresh(false);
        }
    }

    private async void LocationChanged(object sender, LocationChangedEventArgs args)
    {
        _absoluteUri = args.Location;
        _isInternalNavigation = true;
        await Refresh(true);
    }

    Task IHandleAfterRender.OnAfterRenderAsync()
    {
        if (!_navigationInterceptionEnabled)
        {
            _navigationInterceptionEnabled = true;
            return NavigationInterception.EnableNavigationInterceptionAsync();
        }
        return Task.CompletedTask;
    }

    [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")]
    private async Task Refresh(bool locationChanged)
    {
        Site site = null;
        Page page = null;
        List<Module> modules = null;
        User user = null;
        var editmode = false;
        var refresh = false;
        var lastsyncdate = DateTime.MinValue;
        var visitorId = -1;
        var renderid = Guid.Empty;
        _error = "";

        Route route = new Route(_absoluteUri, SiteState.Alias.Path);
        int moduleid = int.Parse(route.ModuleId, CultureInfo.InvariantCulture);
        var action = route.Action; 

        var querystring = Utilities.ParseQueryString(route.Query);
        var returnurl = "";
        if (querystring.ContainsKey("returnurl"))
        {
            returnurl = WebUtility.UrlDecode(querystring["returnurl"]);
            if (!returnurl.StartsWith("/"))
            {
                // urls which are not relative are vulnerable to open redirects or XSS
                returnurl = "";
                querystring["returnurl"] = "";
            }
        }

        // reload the client application from the server if there is a forced reload
        if (querystring.ContainsKey("reload"))
        {
            if (querystring.ContainsKey("reload") && querystring["reload"] == "post")
            {
                if (PageState.RenderMode == RenderModes.Interactive)
                {
                    // post back so that the cookies are set correctly - required on any change to the principal
                    var interop = new Interop(JSRuntime);
                    var fields = new { returnurl = "/" + NavigationManager.ToBaseRelativePath(_absoluteUri) };
                    string url = Utilities.TenantUrl(SiteState.Alias, "/pages/external/");
                    await interop.SubmitForm(url, fields);
                    return;
                }
                else
                {
                    NavigationManager.NavigateTo(_absoluteUri.Replace("?reload=post", "").Replace("&reload=post", ""), true);
                    return;
                }
            }
            else
            {
                NavigationManager.NavigateTo(_absoluteUri.Replace("?reload", "").Replace("&reload", ""), true);
                return;				
            }
        }

        // the refresh parameter is used to refresh the client-side PageState
        if (querystring.ContainsKey("refresh"))
        {
            refresh = true;
        }

        // verify user is authenticated for current site
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        if (authState.User.IsAuthenticated() && authState.User.SiteKey() == SiteState.Alias.SiteKey)
        {
            if (PageState == null || PageState.User == null || PageState.User.UserId != authState.User.UserId())
            {
                // get user
                user = await UserService.GetUserAsync(authState.User.UserId(), SiteState.Alias.SiteId);
            }
            else
            {
                user = PageState.User;
            }
            if (user != null)
            {
                user.IsAuthenticated = authState.User.Identity.IsAuthenticated;
            }
        }

        if (PageState != null)
        {
            editmode = PageState.EditMode;
            lastsyncdate = PageState.LastSyncDate;
            visitorId = PageState.VisitorId;
        }

        if (PageState != null && PageState.RenderMode == RenderModes.Interactive)
        {
            // process any sync events (for synchronizing the client application with the server)
            var sync = await SyncService.GetSyncEventsAsync(lastsyncdate);
            lastsyncdate = sync.SyncDate;
            if (sync.SyncEvents.Any())
            {
                // reload client application if server was restarted
                if (sync.SyncEvents.Exists(item => item.Action == SyncEventActions.Reload && item.EntityName == EntityNames.Host))
                {
                    NavigationManager.NavigateTo(_absoluteUri, true);
                    return;
                }
                // refresh PageState when site information has changed
                if (sync.SyncEvents.Exists(item => item.Action == SyncEventActions.Refresh && item.EntityName == EntityNames.Site && item.EntityId == SiteState.Alias.SiteId))
                {
                    refresh = true;
                }
            }
        }

        // get site
        if (PageState == null || refresh || PageState.Alias.SiteId != SiteState.Alias.SiteId)
        {
            site = await SiteService.GetSiteAsync(SiteState.Alias.SiteId);
            refresh = true;
        }
        else
        {
            site = PageState.Site;
        }

        if (site != null)
        {
            if (Runtime == Runtimes.Hybrid && !site.Hybrid)
            {
                _error = "Hybrid Integration Is Not Enabled For This Site";
                return;
            }

            if (refresh || PageState == null || PageState.Page.Path != route.PagePath)
            {
                page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase));
            }
            else
            {
                page = PageState.Page;
            }

            if (page == null && route.PagePath == "") // naked path refers to site home page
            {
                if (site.HomePageId != null)
                {
                    page = site.Pages.FirstOrDefault(item => item.PageId == site.HomePageId);
                }
                if (page == null)
                {
                    // fallback to use the first page in the collection
                    page = site.Pages.FirstOrDefault();
                }
            }
            if (page == null)
            {
                page = await PageService.GetPageAsync(route.PagePath, site.SiteId);
            }
            else
            {
                // look for personalized page
                if (user != null && page.IsPersonalizable && !UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList))
                {
                    var settingName = $"PersonalizedPagePath:{page.SiteId}:{page.PageId}";
                    var path = (user.Settings.ContainsKey(settingName)) ? user.Settings[settingName] : Utilities.GetFriendlyUrl(user.Username);
                    var personalized = await PageService.GetPageAsync(route.PagePath + "/" + path, site.SiteId);
                    if (personalized != null)
                    {
                        // redirect to the personalized page
                        NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, personalized.Path, ""), false);
                        return;
                    }
                }
            }

            // check if user is authorized to view page
            if (page != null && UserSecurity.IsAuthorized(user, PermissionNames.View, page.PermissionList) && (Utilities.IsEffectiveAndNotExpired(page.EffectiveDate, page.ExpiryDate) || UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList)))
            {
                // edit mode
                if (user != null)
                {
                    var editpageid = user.Settings.ContainsKey("CP-editmode") ? int.Parse(user.Settings["CP-editmode"], CultureInfo.InvariantCulture) : -1;
                    if ((querystring.ContainsKey("edit") && querystring["edit"] == "true") || page.PageId == editpageid)
                    {
                        editmode = true;
                    }
                    else
                    {
                        if (editpageid != -1)
                        {
                            // reset edit mode page
                            await SettingService.AddOrUpdateSettingAsync(EntityNames.User, user.UserId, "CP-editmode", "-1", false);
                        }
                    }
                }

                // get modules for current page
                if (refresh || PageState.Modules == null || !PageState.Modules.Any() || PageState.Modules.First().PageId != page.PageId)
                {
                    modules = await SiteService.GetModulesAsync(site.SiteId, page.PageId);
                }
                else
                {
                    modules = PageState.Modules;
                }

                // renderid allows the framework to determine which module components should be rendered on a page
                if (PageState == null || locationChanged)
                {
                    renderid = Guid.NewGuid();
                }
                else
                {
                    renderid = PageState.RenderId;
                }
                
                // load additional metadata for current page
                page = ProcessPage(page, site, user, SiteState.Alias, action);

                // load additional metadata for modules
                (page, modules) = ProcessModules(site, page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias, renderid);

                //cookie consent
                var _allowCookies = PageState?.AllowCookies;
                if(!_allowCookies.HasValue)
                {
                    var cookieConsentSettings = SettingService.GetSetting(site.Settings, "CookieConsent", string.Empty);
                    _allowCookies = string.IsNullOrEmpty(cookieConsentSettings) || await CookieConsentService.CanTrackAsync(cookieConsentSettings == "optout");
                }
                
                // populate page state (which acts as a client-side cache for subsequent requests)
                _pagestate = new PageState
                {
                    Alias = SiteState.Alias,
                    Site = site,
                    Page = page,
                    Modules = modules,
                    User = user,
                    Uri = new Uri(_absoluteUri, UriKind.Absolute),
                    Route = route,
                    QueryString = querystring,
                    UrlParameters = route.UrlParameters,
                    ModuleId = moduleid,
                    Action = action,
                    EditMode = editmode,
                    LastSyncDate = lastsyncdate,
                    RenderMode = RenderMode,
                    Runtime = (Shared.Runtime)Enum.Parse(typeof(Shared.Runtime), Runtime),
					VisitorId = visitorId,
					RemoteIPAddress = SiteState.RemoteIPAddress,
					ReturnUrl = returnurl,
                    IsInternalNavigation = _isInternalNavigation,
                    RenderId = renderid,
                    Refresh = false,
                    AllowCookies = _allowCookies.GetValueOrDefault(true)
                };
                OnStateChange?.Invoke(_pagestate);

                if (PageState.RenderMode == RenderModes.Interactive)
                {
                    await ScrollToFragment(_pagestate.Uri);
                }
            }
            else
            {
                if (page == null)
                {
                    // check for url mapping
                    var urlMapping = await UrlMappingService.GetUrlMappingAsync(site.SiteId, route.PagePath);
                    if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
                    {
                        var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + (!urlMapping.MappedUrl.StartsWith("/") ? "/" : "") + urlMapping.MappedUrl + ((!urlMapping.MappedUrl.Contains("?")) ? route.Query : "");
                        NavigationManager.NavigateTo(url, false);
                        return;
                    }
                }
                else
                {
                    if (user == null)
                    {
                        // redirect to login page if user not logged in as they may need to be authenticated
                        NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "login", "?returnurl=" + WebUtility.UrlEncode(route.PathAndQuery)));
                        return;
                    }
                }

                // page not found or user does not have sufficient access
                if (route.PagePath != "404")
                {
                    // redirect to 404 page
                    NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "404", ""));
                }
                else
                {
                    // redirect to home page as a fallback
                    NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "", ""));
                }
            }
        }
        else
        {
            // site does not exist
        }
    }

    private Page ProcessPage(Page page, Site site, User user, Alias alias, string action)
    {
        try
        {
            page.Panes = new List<string>();
            page.Resources = new List<Resource>();

            // validate theme
            if (string.IsNullOrEmpty(page.ThemeType))
            {
                page.ThemeType = site.DefaultThemeType;
            }
            var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == page.ThemeType));
            if (theme == null)
            {
                // fallback to default Oqtane theme
                page.ThemeType = Constants.DefaultTheme;
                theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == page.ThemeType));
            }
            Type themetype = Type.GetType(page.ThemeType);
            string panes = "";
            if (themetype != null)
            {
                // get resources for theme (ITheme)
                page.Resources = ManagePageResources(page.Resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Fingerprint);

                var themeobject = Activator.CreateInstance(themetype) as IThemeControl;
                if (themeobject != null)
                {
                    if (!string.IsNullOrEmpty(themeobject.Panes))
                    {
                        panes = themeobject.Panes;
                    }
                    // get resources for theme control
                    page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace, theme.Fingerprint);
                }
            }
            // theme settings components are dynamically loaded within the framework Page Management module
            if (page.Path == "admin/pages" && action.ToLower() == "edit" && theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType))
            {
                var settingsType = Type.GetType(theme.ThemeSettingsType);
                if (settingsType != null)
                {
                    var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
                    page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Fingerprint);
                }
            }

            if (!string.IsNullOrEmpty(panes))
            {
                page.Panes = panes.Replace(";", ",").Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();
                if (!page.Panes.Contains(PaneNames.Default) && !page.Panes.Contains(PaneNames.Admin))
                {
                    _error = "The Current Theme Does Not Contain A Default Or Admin Pane";
                }
            }
            else
            {
                page.Panes.Add(PaneNames.Admin);
                _error = "The Current Theme Does Not Contain Any Panes";
            }
        }
        catch
        {
            // error loading theme or layout
        }

        return page;
    }

    private (Page Page, List<Module> Modules) ProcessModules(Site site, Page page, List<Module> modules, int moduleid, string action, string defaultcontainertype, Alias alias, Guid renderid)
    {
        var paneindex = new Dictionary<string, int>();

        foreach (Module module in modules.Where(item => item.PageId == page.PageId || item.ModuleId == moduleid))
        {
            var typename = Constants.ErrorModule;

            // initialize module control properties
            module.SecurityAccessLevel = SecurityAccessLevel.Host;
            module.ControlTitle = "";
            module.Actions = "";
            module.UseAdminContainer = false;
            module.PaneModuleIndex = -1;
            module.PaneModuleCount = 0;

            if (module.ModuleDefinition != null && (module.ModuleDefinition.Runtimes == "" || module.ModuleDefinition.Runtimes.Contains(Runtime)))
            {
                page.Resources = ManagePageResources(page.Resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Fingerprint);

                // handle default action
                if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
                {
                    action = module.ModuleDefinition.DefaultAction;
                }

                // get typename template
                typename = module.ModuleDefinition.ControlTypeTemplate;
                if (module.ModuleDefinition.ControlTypeRoutes != "")
                {
                    // process custom action routes
                    foreach (string route in module.ModuleDefinition.ControlTypeRoutes.Split(';', StringSplitOptions.RemoveEmptyEntries))
                    {
                        if (route.StartsWith(action + "="))
                        {
                            typename = route.Replace(action + "=", "");
                            break;
                        }
                    }
                }
            }

            // create typename
            if (Constants.DefaultModuleActions.Contains(action, StringComparer.OrdinalIgnoreCase))
            {
                typename = Constants.DefaultModuleActionsTemplate.Replace(Constants.ActionToken, action);
            }
            else
            {
                typename = typename.Replace(Constants.ActionToken, action);
            }

            // ensure component exists and implements IModuleControl
            module.ModuleType = "";
            Type moduletype = Type.GetType(typename, false, true); // case insensitive
            if (moduletype != null && moduletype.GetInterfaces().Contains(typeof(IModuleControl)))
            {
                module.ModuleType = Utilities.GetFullTypeName(moduletype.AssemblyQualifiedName); // get actual type name
            }

            if (moduletype != null && module.ModuleType != "")
            {
                // retrieve module component resources
                var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
                module.RenderMode = moduleobject.RenderMode;
                module.Prerender = moduleobject.Prerender;

                page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint);

                // settings components are dynamically loaded within the framework Settings module
                if (action.ToLower() == "settings" && module.ModuleDefinition != null)
                {
                    // module settings component
                    var settingsType = "";
                    if (!string.IsNullOrEmpty(module.ModuleDefinition.SettingsType))
                    {                        
                        // module settings type explicitly declared in IModule interface
                        settingsType = module.ModuleDefinition.SettingsType; 
                    }
                    else
                    {
                        // legacy support - module settings type determined by convention
                        settingsType = module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action);
                    }
                    moduletype = Type.GetType(settingsType, false, true);
                    if (moduletype != null)
                    {
                        moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
                        page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint);
                    }

                    // container settings component
                    var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == page.ThemeType));
                    if (theme != null && !string.IsNullOrEmpty(theme.ContainerSettingsType))
                    {
                        moduletype = Type.GetType(theme.ContainerSettingsType);
                        if (moduletype != null)
                        {
                            moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
                            page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Fingerprint);
                        }
                    }
                }

                // additional metadata needed for admin components
                if (module.ModuleId == moduleid && action != "")
                {
                    module.ControlTitle = moduleobject.Title;
                    module.SecurityAccessLevel = moduleobject.SecurityAccessLevel;
                    module.Actions = moduleobject.Actions;
                    module.UseAdminContainer = moduleobject.UseAdminContainer;
                }
            }

            // validate that module's pane exists in current page
            if (page.Panes.FindIndex(item => item.Equals(module.Pane, StringComparison.OrdinalIgnoreCase)) == -1)
            {
                // fallback to default pane if it exists
                if (page.Panes.FindIndex(item => item.Equals(PaneNames.Default, StringComparison.OrdinalIgnoreCase)) != -1)
                {
                    module.Pane = PaneNames.Default;
                }
                else // otherwise admin pane (legacy)
                {
                    module.Pane = PaneNames.Admin;						
                }
            }

            // calculate module position within pane
            if (paneindex.ContainsKey(module.Pane.ToLower()))
            {
                paneindex[module.Pane.ToLower()] += 1;
            }
            else
            {
                paneindex.Add(module.Pane.ToLower(), 0);
            }

            module.PaneModuleIndex = paneindex[module.Pane.ToLower()];

            // container fallback
            if (string.IsNullOrEmpty(module.ContainerType))
            {
                module.ContainerType = defaultcontainertype;
            }

            module.RenderId = renderid;
        }

        foreach (Module module in modules.Where(item => item.PageId == page.PageId))
        {
            if (paneindex.ContainsKey(module.Pane.ToLower()))
            {
                module.PaneModuleCount = paneindex[module.Pane.ToLower()] + 1;
            }
        }

        return (page, modules);
    }

    private List<Resource> ManagePageResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name, string fingerprint)
    {
        if (resources != null)
        {
            foreach (var resource in resources)
            {
                if (resource.ResourceType == ResourceType.Stylesheet || resource.Level != ResourceLevel.Site)
                {
                    if (resource.Url.StartsWith("~"))
                    {
                        resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/");
                    }
                    if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
                    {
                        resource.Url = alias.BaseUrl + resource.Url;
                    }

                    // ensure resource does not exist already
                    if (!pageresources.Exists(item => Utilities.GetUrlPath(item.Url).ToLower() == Utilities.GetUrlPath(resource.Url).ToLower()))
                    {
                        pageresources.Add(resource.Clone(level, name, fingerprint));
                    }
                }
			}
		}
		return pageresources;
	}

	private async Task ScrollToFragment(Uri uri)
	{
		var fragment = uri.Fragment;
		if (fragment.StartsWith('#'))
		{
			// handle text fragment (https://example.org/#test:~:text=foo)
			var id = fragment.Substring(1);
			var index = id.IndexOf(":~:", StringComparison.Ordinal);
			if (index > 0)
			{
				id = id.Substring(0, index);
			}

			if (!string.IsNullOrEmpty(id))
			{
				var interop = new Interop(JSRuntime);
				await interop.ScrollToId(id);
			}
        }
    }
}
